##
# Method objects are essentially detached, freely passed-around methods. The
# Method is a copy of the method on the object at the time of extraction, so
# if the method itself is changed, overridden, aliased or removed from the
# object, the Method object still contains the old functionality. In addition,
# the call itself is not in any way stored so it will reflect the state of the
# object at the time of calling.
#
# Methods are normally bound to a particular object but it is possible to use
# Method#unbind to create an UnboundMethod object for the purpose of
# re-binding to a different object.

class Method

  ##
  # Takes and stores the receiver object, the method's bytecodes and the
  # Module that the method is defined in.

  def initialize(receiver, defined_in, executable, name)
    @receiver    = receiver
    @defined_in  = defined_in
    @executable  = executable
    @name        = name
  end

  private :initialize

  attr_reader :receiver
  attr_reader :defined_in
  attr_reader :executable

  def name
    @name
  end

  ##
  # Method objects are equal if they have the same body and are bound to the
  # same object.

  def ==(other)
    other.class == Method &&
      @receiver.equal?(other.receiver) &&
      (@executable == other.executable ||
       Rubinius::MethodEquality.method_equal_to_delegated_method_receiver?(self, other))
  end

  alias_method :eql?, :==

  ##
  # Indication of how many arguments this method takes. It is defined so that
  # a non-negative Integer means the method takes that fixed amount of
  # arguments (up to 1024 currently.) A negative Integer is used to indicate a
  # variable argument count. The number is ((-n) - 1), where n is the number
  # of required args. Blocks are not counted.
  #
  #   def foo();             end   # arity => 0
  #   def foo(a, b);         end   # arity => 2
  #   def foo(a, &b);        end   # arity => 1
  #   def foo(a, b = nil);   end   # arity => ((-1) -1) => -2
  #   def foo(*a);           end   # arity => ((-0) -1) => -1
  #   def foo(a, b, *c, &d); end   # arity => ((-2) -1) => -3

  def arity
    @executable.arity
  end

  ##
  # Execute the method. This works exactly like calling a method with the same
  # code on the receiver object. Arguments and a block can be supplied
  # optionally.

  def call(*args, &block)
    @executable.invoke(@name, @defined_in, @receiver, args, block)
  end

  alias_method :[], :call

  ##
  # String representation of this Method includes the method name, the Module
  # it is defined in and the Module that it was extracted from.

  def inspect
    file, line = source_location()

    if @executable.respond_to? :eval_source
      if src = @executable.eval_source
        src_line = src.split("\n")[line - 1]
        return "#<#{self.class}: #{@receiver.class}##{@name} (defined in #{@defined_in} from \"#{src_line}\")>"
      end
    end

    if file
      "#<#{self.class}: #{@receiver.class}##{@name} (defined in #{@defined_in} at #{file}:#{line})>"
    else
      "#<#{self.class}: #{@receiver.class}##{@name} (defined in #{@defined_in})>"
    end
  end

  alias_method :to_s, :inspect

  ##
  # Location gives the file and line number of the start of this method's
  # definition.

  def source_location
    if @executable.respond_to? :file
      [@executable.file.to_s, @executable.defined_line]
    elsif @executable.respond_to? :source_location
      @executable.source_location
    else
      nil
    end
  end

  def source
    if @executable.respond_to? :eval_source
      return @executable.eval_source
    end

    return nil
  end

  def parameters
    if @executable.respond_to? :parameters
      @executable.parameters
    else
      []
    end
  end

  def owner
    if @defined_in.class == Rubinius::IncludedModule
      @defined_in.module
    else
      @defined_in
    end
  end

  ##
  # Returns a Proc object corresponding to this Method.

  def to_proc
    Proc.from_method self
  end

  ##
  # Calls curry on the method in proc representation
  def curry(n = nil)
    to_proc.curry(n)
  end

  ##
  # Detach this Method from the receiver object it is bound to and create an
  # UnboundMethod object. Populates the UnboundMethod with the method data as
  # well as the Module it is defined in and the Module it was extracted from.
  #
  # See UnboundMethod for more information.

  def unbind
    UnboundMethod.new(@defined_in, @executable, @receiver.class, @name)
  end

  def super_method
    superclass = @defined_in.direct_superclass

    if superclass
      mod, entry = superclass.lookup_method(@name)

      if entry && entry.visibility != :undef
        return Method.new(@receiver, superclass, entry.method, @name)
      end
    end

    return nil
  end

  def for_define_method(name, klass, callable_proc = nil)
    Rubinius::Type.bindable_method? self.defined_in, klass

    scope = @executable.scope

    if @executable.is_a? Rubinius::DelegatedMethod
      code = @executable
    else
      if callable_proc
        code = Rubinius::DelegatedMethod.new(name, :call, callable_proc, false)
      else
        code = Rubinius::DelegatedMethod.new(name, :call_on_instance, self.unbind, true)
      end
    end

    [code, scope]
  end
end

##
# UnboundMethods are similar to Method objects except that they are not
# connected to any particular object. They cannot be used standalone for this
# reason, and must be bound to an object first. The object must be kind_of?
# the Module in which this method was originally defined.
#
# UnboundMethods can be created in two ways: first, any existing Method object
# can be sent #unbind to detach it from its current object and return an
# UnboundMethod instead. Secondly, they can be directly created by calling
# Module#instance_method with the desired method's name.
#
# The UnboundMethod is a copy of the method as it existed at the time of
# creation. Any subsequent changes to the original will not affect any
# existing UnboundMethods.

class UnboundMethod

  ##
  # Accepts and stores the Module where the method is defined in as well as
  # the CompiledCode itself. Class of the object the method was extracted
  # from can be given but will not be stored. This is always used internally
  # only.

  def initialize(mod, executable, pulled_from, name)
    @defined_in  = mod
    @executable  = executable
    @pulled_from = pulled_from
    @name        = name
  end

  private :initialize

  attr_reader :executable
  attr_reader :defined_in

  def name
    @name
  end

  ##
  # Convenience method for #binding to the given receiver object and calling
  # it with the optionally supplied arguments.

  def call_on_instance(obj, *args, &block)
    @executable.invoke(@name, @defined_in, obj, args, block)
  end

  ##
  # UnboundMethod objects are equal if and only if they refer to the same
  # method. One may be an alias for the other or both for a common one. Both
  # must have been extracted from the same class or subclass. Two from
  # different subclasses will not be considered equal.

  def ==(other)
    other.kind_of? UnboundMethod and
      @defined_in == other.defined_in and
      @executable == other.executable
  end

  def hash
    @defined_in.hash ^ @executable.hash
  end

  ##
  # See Method#arity.

  def arity
    @executable.arity
  end

  ##
  # Creates a new Method object by attaching this method to the supplied
  # receiver. One of the following must be true:
  # - the method was defined in a module
  # - the method is being bound to an instance of a subclass of the method's source
  # - the method is being bound to an instance of the same class

  def bind(receiver)
    from_module = Rubinius::Type.object_kind_of? @defined_in, Module
    from_class  = Rubinius::Type.object_kind_of? @defined_in, Class
    from_module_not_class = from_module && !from_class

    unless Rubinius::Type.object_kind_of?(receiver, @defined_in) or from_module_not_class
      if Rubinius::Type.singleton_class_object(@defined_in)
        raise TypeError, "illegal attempt to rebind a singleton method to another object"
      end

      raise TypeError, "Must be bound to an object of kind #{@defined_in}"
    end

    Method.new receiver, @defined_in, @executable, @name
  end

  ##
  # String representation for UnboundMethod includes the method name, the
  # Module it is defined in and the Module that it was extracted from.

  def inspect
    file, line = source_location()

    if file
      "#<#{self.class}: #{@pulled_from}##{@name} (defined in #{@defined_in} at #{file}:#{line})>"
    else
      "#<#{self.class}: #{@pulled_from}##{@name} (defined in #{@defined_in})>"
    end
  end

  alias_method :to_s, :inspect

  def source_location
    if @executable.respond_to? :file
      [@executable.file.to_s, @executable.defined_line]
    elsif @executable.respond_to? :source_location
      @executable.source_location
    else
      nil
    end
  end

  def parameters
    return @executable.respond_to?(:parameters) ? @executable.parameters : []
  end

  def owner
    if @defined_in.class == Rubinius::IncludedModule
      @defined_in.module
    else
      @defined_in
    end
  end

  def super_method
    superclass = @defined_in.direct_superclass

    if superclass
      mod, entry = superclass.lookup_method(@name)

      if entry && entry.visibility != :undef
        return UnboundMethod.new(superclass, entry.method, @defined_in, @name)
      end
    end

    return nil
  end

  def for_define_method(name, klass, callable_proc = nil)
    Rubinius::Type.bindable_method? self.defined_in, klass

    case @executable
    when Rubinius::AccessVariable
      code = @executable
      scope = nil
    when Rubinius::DelegatedMethod
      code = @executable
      scope = code.scope
    else
      if callable_proc
        code = Rubinius::DelegatedMethod.new(name, :call, callable_proc, false)
      else
        code = Rubinius::DelegatedMethod.new(name, :call_on_instance, self, true)
      end
      scope = @executable.scope
    end

    [code, scope]
  end
end
